package com.qianlima.upload.domain.service.impl;

import com.qianlima.upload.common.constants.Constants;
import com.qianlima.upload.common.dto.FileDto;
import com.qianlima.upload.domain.service.BaseFileAbstract;
import com.qianlima.upload.domain.service.IFileService;
import com.qianlima.upload.infrastructure.utils.Base64ToMultipartFile;
import com.qianlima.upload.infrastructure.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * @description: 文件上传实现类
 * @author：allms
 * @date: 2023/3/22 15:23
 */
@Service("fileUploadService")
@Slf4j
public class FileServiceUploadImpl extends BaseFileAbstract implements IFileService {

    @Resource
    private RedisUtils redisUtils;

    private void pushShardIndex(String key, Integer shardIndex) {
        redisUtils.add(key, shardIndex);

    }

    private boolean isExistShard(String key, Integer shardIndex) {
        if (!redisUtils.exists(key)) {
            return false;
        }
        Set<Object> resultList = redisUtils.setMembers(key);
        return resultList.stream()
                .map(obj -> Integer.parseInt(obj.toString())
                ).filter(shard -> shard.equals(shardIndex))
                .collect(Collectors.toSet()).size() > 0;
    }

    private List<Integer> getNotExistShardIndexList(String key, Integer shardTotal) {
        if (!redisUtils.exists(key)) {
            return new ArrayList<>();
        }
        Set<Object> resultList = redisUtils.setMembers(key);
        // 已保存的分片
        Set<Integer> set = resultList.stream()
                .map(obj -> Integer.parseInt(obj.toString())).collect(Collectors.toSet());
        // 原始分片
        List<Integer> nums = IntStream.rangeClosed(1, shardTotal)
                .boxed()
                .collect(Collectors.toList());
        // 未上传分片
        return nums.stream()
                .distinct()
                .filter(num -> !set.contains(num))
                .sorted()
                .collect(Collectors.toList());
    }

    private void saveShardFileToLocal(MultipartFile shardFile, String key, String localPath, Integer shardIndex) throws IOException {
        File shardFileDir = new File(fileGlobalConfiguration.getLocal().getUploadPath());
        if (!shardFileDir.exists()) {
            boolean mkdir = shardFileDir.mkdir();
            if (!mkdir) {
                throw new IOException();
            }
        }
        // 源文件.后缀:small/6sfSqfOwzmik4A4icMYuUe.mp4.1
        String fullPath = localPath + "." + shardIndex;
        File dest = new File(fullPath);
        shardFile.transferTo(dest);
        log.info("文件：{} ，分片：{}，位置：{}", key, shardIndex, dest.getAbsolutePath());
    }


    @Override
    public List<String> upload(List<MultipartFile> multipartFileList) throws IOException {
        return super.upload(multipartFileList);
    }

    @Override
    public String fragUpload(FileDto fileDto, boolean isFile, MultipartFile file) throws IOException, InterruptedException {
        log.info("======上传文件开始=======");
        fileDto.verify();
        // 文件key，这儿是个md5
        String key = fileDto.getFileKey();

        FileDto sysFile = this.findByKey(key);
        if(Objects.nonNull(sysFile)) {
            log.info("文件：{} 秒传成功", key);
            return fileGlobalConfiguration.getLocal().getUrl() + sysFile.getPath();
        }

        // 1.==============保存分片到本地
        // 分片索引
        Integer shardIndex = fileDto.getShardIndex();
        // 检查分片是否存在
        if (this.isExistShard(key, shardIndex)) {
            log.info("文件：{}, 分片：{}已经上传", key, shardIndex);
            return null;
        }
        // 分片Base64
        String shardBase64 = fileDto.getShard();
        // 分片shard
        MultipartFile shard = isFile ? file : Base64ToMultipartFile.base64ToMultipart(shardBase64);
        // 文件后缀
        String suffix = fileDto.getSuffix();
        // 本地路径
        String filePath =  "/" + key + "." + suffix;
        String localPath = fileGlobalConfiguration.getLocal().getUploadPath() + filePath;
        this.saveShardFileToLocal(shard, key, localPath, shardIndex);


        // 2.-------------保存分片索引到缓存
        pushShardIndex(key, shardIndex);

        //  3.-----------判断所有分片上传完毕
        if (redisUtils.setMembers(key).size() == fileDto.getShardTotal()) {
            // 4.-------------保存到数据库
            log.info("======================保存DB记录开始=====================");
            fileDto.setPath(fileGlobalConfiguration.getLocal().getBucketName() + filePath);
            fileDto.setFileKey(key);
            if (this.saveOrUpdateFile(fileDto)) {
                // 执行文件分片
                this.mergeFiles(localPath, fileDto.getShardTotal());
                // 删除缓存
                redisUtils.remove(key);
                return fileGlobalConfiguration.getLocal().getUrl() + filePath;
            }
        }
        return null;
    }

    /**
     * 分片合并
     *
     * @param localPath  文件路径 samll/6sfSqfOwzmik4A4icMYuUe.mp4
     * @param shardTotal 分片总大小
     * @throws IOException
     * @throws InterruptedException
     */
    private void mergeFiles(String localPath, Integer shardTotal) throws IOException, InterruptedException {
        log.info("=============合并分片开始==========================");
        File newFile = new File(localPath);
        // 文件追加写入
        FileOutputStream outputStream = new FileOutputStream(newFile, true);
        //分片文件
        FileInputStream fileInputStream = null;
        byte[] byt = new byte[10 * 1024 * 1024];
        int len;

        try {
            for (int i = 0; i < shardTotal; i++) {
                // 读取第i个分片course\6sfSqfOwzmik4A4icMYuUe.mp4.1
                fileInputStream = new FileInputStream(localPath + "." + (i + 1));
                while ((len = fileInputStream.read(byt)) != -1) {
                    outputStream.write(byt, 0, len);
                }
            }
        } catch (IOException e) {
            log.error("分片合并异常", e);
            throw e;
        } finally {
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                outputStream.close();
                log.info("IO流关闭");
            } catch (Exception e) {
                log.error("IO流关闭", e);
            }
        }
        log.info("合并分片结束");

        System.gc();
        Thread.sleep(100);

        // 删除分片
        log.info("删除分片开始");
        for (int i = 0; i < shardTotal; i++) {
            String filePath = localPath + "." + (i + 1);
            File file = new File(filePath);
            boolean result = file.delete();
            log.info("删除{}，{}", filePath, result ? "成功" : "失败");
        }
        log.info("删除分片结束");
    }

    @Override
    public FileDto checkFrag(String key, Integer shardTotal) {
        log.info("===============检查分片开始==================");
        log.info("检查上传分片开始：{}", key);
        FileDto fileDto = findByKey(key.toLowerCase());
        // 极速秒传
        if (fileDto != null) {
            fileDto.setPath(fileGlobalConfiguration.getLocal().getUrl(false) + fileDto.getPath());
            fileDto.setNotExistShardIndexList(new ArrayList<>());
            fileDto.setShardIndex(shardTotal);
            return fileDto;
        }
        // 已经有分片
        if (redisUtils.exists(key)) {
            FileDto result = new FileDto();
            result.setNotExistShardIndexList(getNotExistShardIndexList(key, shardTotal));
            return result;
        }
        log.info("===============检查分片结束==================");
        // 初始化分片
        return new FileDto().setNotExistShardIndexList(IntStream.rangeClosed(1, shardTotal)
                .boxed()
                .collect(Collectors.toList()));
    }

    @Override
    public String getType() {
        return Constants.FileStrategyMode.Local.getCode();
    }
}
